Skip to content

Conversation

@Bromeon
Copy link
Member

@Bromeon Bromeon commented Nov 2, 2025

Following up #1278, this PR introduces two new assertion groups:

  • 🛡️ sys::strict_assert! -- validate in strict level only
  • ⚖️ sys::balanced_assert! -- validate in strict+balanced levels

This replaces a majority of debug_asserts in the library. The idea is to slowly move away from them.

Regular assert! will still be used, whenever all levels need to be validated, concretely:

  • When it's a public API contract to panic (although this may evolve).
  • When it's not just a matter of unsafety but unsoundness -- i.e. the user would run into UB even if they write correct code, due to runtime conditions not in their control.

The PR should not introduce major semantic changes, however it's very likely that conditions previously not validated are validated now, and vice versa, depending on your level.

More work is planned in the future, like turning off more checks in disengaged level.

@Bromeon Bromeon added the quality-of-life No new functionality, but improves ergonomics/internals label Nov 2, 2025
@Bromeon Bromeon requested review from TitanNano and Yarwin November 2, 2025 16:13
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1395

@Bromeon Bromeon force-pushed the qol/safeguard-assertions branch 2 times, most recently from 93e94b3 to 7741e64 Compare November 2, 2025 20:13
rust_function,
#[cfg(debug_assertions)]
Self::from_fn_wrapper(
name,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function docs must be updated:

name is used for the string representation of the closure, which helps with debugging.

While putting it under safeguards makes sense, I would expect debugging info to be controlled by debug/release mode.

Same in other places

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While putting it under safeguards makes sense, I would expect debugging info to be controlled by debug/release mode.

Strictly speaking there is no Debug/Release mode, only debug_assertions being on/off -- but yes, this typically corresponds to Rust's dev/release profiles, unless changed. However, in practice it's more "running in Godot editor or release-template", which is a runtime decision.

Generally I'd like to move away from debug_assertions, because safeguards are more flexible. This particular change was also motivated by performance alone: #1331

It might be an option to make the name still available in all safeguard levels, by simply storing it as Cow<str> or so, and access it only when needed (e.g. to_string). This could avoid a lot of allocations and conversions, because most users likely use literals here. Such a change would be out-of-scope for this PR though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree, that's why I mentioned that it makes sense – I would only update function docs, to inform when name is being optimized out 😅.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create a follow-up PR and do it there, maybe we can avoid optimizing it out 🙂

/// # Panics
/// In Debug mode, if `index` is out of bounds. In Release, a Godot error is generated and the result is unspecified (but safe).
/// # Panics (safeguards-balanced)
/// If `index` is out of bounds. In safeguards-disengaged mode, a Godot error is generated and the result is unspecified (but safe).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really related to this PR but what does "safe" mean in this context 🤔?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot cause UB. The behavior is unspecified.

This is explicitly mentioned because in many scenarios, absent validations in safeguards-disengaged immediately cause undefined behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered if we shouldn't call it "sound" but safe is more straightforward and easier to grasp, I think 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be, although "safe" implies "sound".

  • Safe: there can be no UB.
  • Unsafe: it's the user's responsibility to not cause UB.
  • Unsound: there is UB, something needs to be fixed.

/// # Panics
/// In Debug mode, if `index` is out of bounds. In Release, a Godot error is generated and the result is unspecified (but safe).
/// # Panics (safeguards-balanced)
/// If `index` is out of bounds. In safeguards-disengaged mode, a Godot error is generated and the result is unspecified (but safe).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeguards-disengaged mode

level

///
/// # Panics
/// In debug mode, when from > to.
pub(crate) fn to_godot_range_fromto(range: impl SignedRange) -> (i64, i64) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description must be updated too

/// # Panics (safeguards-strict)
/// When `from` > `to`.

}
// Check that we're on the main thread. Only enabled with balanced+ safeguards and in multi-threaded builds.
// In wasm_nothreads, there's only one thread, so no check is needed.
#[cfg(all(safeguards_balanced, not(wasm_nothreads)))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if early return with if cfg!("wasm_nothreads") { return } wouldn't be more readable 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a ensure_main_thread() function to extract it.

  • Early return doesn't gain much because we'd still have to cover #[cfg(...)] across both the return and the remaining logic.
  • The alternative is having 2 functions (one no-op), but that also needs two #[cfg(...)] and much more LoC + noise.
  • Easiest might be cfg! but then I'm not sure if it's fully optimized away, in this method is a bit risky.

@Bromeon Bromeon force-pushed the qol/safeguard-assertions branch from 7741e64 to 5e75d1c Compare November 2, 2025 21:32
@Bromeon Bromeon added this pull request to the merge queue Nov 3, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 3, 2025
@Bromeon Bromeon added this pull request to the merge queue Nov 3, 2025
Merged via the queue into master with commit 53043c9 Nov 3, 2025
20 checks passed
@Bromeon Bromeon deleted the qol/safeguard-assertions branch November 3, 2025 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

quality-of-life No new functionality, but improves ergonomics/internals

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants